package com.abo.yaas.webservice;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
import javax.persistence.OptimisticLockException;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.metamodel.SingularAttribute;

import com.abo.yaas.webservice.constants.Operation;
import com.abo.yaas.webservice.exceptions.WebServiceException;
import com.abo.yaas.webservice.hibernate.EntityManagerUtils;
import com.abo.yaas.webservice.hibernate.SortCriteria;
import com.abo.yaas.webservice.hibernate.WSFilter;
import com.abo.yaas.webservice.messageproviders.ClassWrapper;
import com.abo.yaas.webservice.messageproviders.ListWrapper;

@SuppressWarnings("unchecked")
public class CriteriaQueryUtil {
    private static void applyFilter(CriteriaQuery<?> query, WSFilter<?> filter) {
        if (filter != null) {
            final Predicate pred = filter.getPredicate(getRoot(query));
            if (pred != null) {
                query.where(pred);
            }
        }
    }

    private static <T> void applySortCriteria(CriteriaQuery<?> query, List<SortCriteria<T>> sortCriteria) {
        if (sortCriteria != null) {
            final List<Order> sortCriteriaList = new ArrayList<Order>();
            for (final SortCriteria<T> sc : sortCriteria) {
                sortCriteriaList.add(sc.asOrder((Root<T>) getRoot(query)));
            }
            query.orderBy(sortCriteriaList);
        }
    }

    private static <T> CriteriaQuery<Long> buildCountQuery(Class<T> clazz) {
        final CriteriaQuery<Long> query = getBuilder().createQuery(Long.class);
        final Root<T> root = query.from(clazz);
        query.select(getBuilder().count(root.get(getAttribute("id", clazz))));
        return query;
    }

    private static <T> CriteriaQuery<?> buildIdQuery(Class<T> clazz) {
        final CriteriaQuery<Object> query = getBuilder().createQuery(Object.class);
        final Root<T> root = query.from(clazz);
        query.select(root.get(getAttribute("id", clazz)));
        return query;
    }

    public static <T> ClassWrapper<Boolean> containsEntity(final Object id, final WSFilter<T> filter, Class<T> clazz) {
        Long count = 0l;
        final CriteriaQuery<Long> query = buildCountQuery(clazz);
        filter.addChild(new WSFilter<T>("id", id, Operation.EQ, clazz));
        applyFilter(query, filter);
        final EntityManager em = EntityManagerUtils.getEntityManager();
        final EntityTransaction tx = em.getTransaction();
        try {
            tx.begin();
            count = em.createQuery(query).getSingleResult();
            tx.commit();
        } catch (final Exception e) {
            tx.rollback();
            e.printStackTrace();
        }
        return new ClassWrapper<Boolean>(count > 0);
    }

    private static <T> Object doGetEdgeEntityId(boolean first, final WSFilter<T> filter, final List<SortCriteria<T>> sortCriteria, Class<T> clazz) {
        final EntityManager em = EntityManagerUtils.getEntityManager();
        final CriteriaQuery<?> query = buildIdQuery(clazz);
        applyFilter(query, filter);
        applySortCriteria(query, sortCriteria);
        final EntityTransaction tx = em.getTransaction();
        List<?> resultList = null;
        try {
            tx.begin();
            resultList = em.createQuery(query).getResultList();
            tx.commit();
        } catch (final Exception e) {
            tx.rollback();
        }
        if (resultList.isEmpty()) {
            return null;
        }
        return first ? resultList.get(0) : resultList.get(resultList.size() - 1);
    }

    private static <T> Object doGetSibling(Object id, boolean next, final WSFilter<T> filter, final List<SortCriteria<T>> sortCriteria, Class<T> clazz) {
        final EntityManager em = EntityManagerUtils.getEntityManager();
        final CriteriaQuery<?> query = buildIdQuery(clazz);
        filter.addChild(new WSFilter<T>("id", id, (next ? Operation.GT : Operation.LT), clazz));
        applyFilter(query, filter);
        applySortCriteria(query, sortCriteria);
        final EntityTransaction tx = em.getTransaction();
        List<?> resultList = null;
        try {
            tx.begin();
            resultList = em.createQuery(query).getResultList();
            tx.commit();
        } catch (final Exception e) {
            tx.rollback();
        }
        return resultList.isEmpty() ? null : resultList.get(0);
    }

    private static <T> SingularAttribute<T, ?> getAttribute(String name, Class<T> clazz) {
        return (SingularAttribute<T, ?>) EntityManagerUtils.getEntityManager().getMetamodel().entity(clazz).getAttribute(name);
    }

    private static CriteriaBuilder getBuilder() {
        return EntityManagerUtils.getEntityManager().getCriteriaBuilder();
    }

    public static <T> T getEntity(Object id, Class<T> clazz) {
        final EntityManager em = EntityManagerUtils.getEntityManager();
        final T result = em.find(clazz, id);
        em.detach(result);
        return result;
    }

    public static <T> T getEntityByProperty(String propertyName, Object value, Class<T> clazz) {
        final EntityManager em = EntityManagerUtils.getEntityManager();
        final CriteriaBuilder builder = em.getCriteriaBuilder();
        final CriteriaQuery<T> query = builder.createQuery(clazz);
        final SingularAttribute<T, ?> attr = getAttribute(propertyName, clazz);
        final Root<T> root = query.from(clazz);
        query.select(root);
        query.where(builder.equal(root.get(attr), value));
        final List<T> res = em.createQuery(query).getResultList();
        if (res.isEmpty()) {
            return null;
        }
        return res.get(0);
    }

    public static <T> ClassWrapper<Long> getEntityCount(final WSFilter<T> filter, Class<T> clazz) {
        final CriteriaQuery<Long> query = getBuilder().createQuery(Long.class);
        final Root<T> root = query.from(clazz);
        query.select(getBuilder().count(root.get(getAttribute("id", clazz))));
        applyFilter(query, filter);
        Long count = 0l;
        final EntityManager em = EntityManagerUtils.getEntityManager();
        final EntityTransaction tx = em.getTransaction();
        try {
            tx.begin();
            count = em.createQuery(query).getSingleResult();
            tx.commit();
        } catch (final Exception e) {
            tx.rollback();
            e.printStackTrace();
        }
        return new ClassWrapper<Long>(count);
    }

    public static <T> ClassWrapper<Object> getEntityIdAt(int idx, final WSFilter<T> filter, final List<SortCriteria<T>> sortCriteria, Class<T> clazz) {
        final CriteriaQuery<?> query = buildIdQuery(clazz);
        applyFilter(query, filter);
        applySortCriteria(query, sortCriteria);
        final EntityManager em = EntityManagerUtils.getEntityManager();
        final EntityTransaction tx = em.getTransaction();
        List<?> result = null;
        try {
            tx.begin();
            result = em.createQuery(query).getResultList();
            tx.commit();
        } catch (final Exception e) {
            tx.rollback();
        }
        return new ClassWrapper<Object>(result.size() <= idx ? null : result.get(idx));
    }

    @SuppressWarnings("rawtypes")
    public static <T> ListWrapper<Object> getEntityIdList(final WSFilter<T> filter, final List<SortCriteria<T>> sortCriteria, Class<T> clazz) {
        final CriteriaQuery<?> query = buildIdQuery(clazz);
        applySortCriteria(query, sortCriteria);
        applyFilter(query, filter);
        final EntityManager em = EntityManagerUtils.getEntityManager();
        final EntityTransaction tx = em.getTransaction();
        List result = null;
        try {
            tx.begin();
            result = em.createQuery(query).getResultList();
            tx.commit();
        } catch (final Exception e) {
            tx.rollback();
        }
        return new ListWrapper<Object>(result);
    }

    public static <T> ClassWrapper<Object> getFirstEntityId(final WSFilter<T> filter, final List<SortCriteria<T>> sortCriteria, Class<T> clazz) {
        return new ClassWrapper<Object>(doGetEdgeEntityId(true, filter, sortCriteria, clazz));
    }

    public static <T> ClassWrapper<Object> getLastEntityId(final WSFilter<T> filter, final List<SortCriteria<T>> sortCriteria, Class<T> clazz) {
        return new ClassWrapper<Object>(doGetEdgeEntityId(false, filter, sortCriteria, clazz));
    }

    public static <T> ClassWrapper<Object> getNextEntityId(Object id, final WSFilter<T> filter, final List<SortCriteria<T>> sortCriteria, Class<T> clazz) {
        return new ClassWrapper<Object>(doGetSibling(id, true, filter, sortCriteria, clazz));
    }

    public static <T> ClassWrapper<Object> getPreviousEntityId(Object id, final WSFilter<T> filter, final List<SortCriteria<T>> sortCriteria, Class<T> clazz) {
        return new ClassWrapper<Object>(doGetSibling(id, false, filter, sortCriteria, clazz));
    }

    private static Root<?> getRoot(CriteriaQuery<?> criteria) {
        final Set<Root<?>> roots = criteria.getRoots();
        if (roots.isEmpty()) {
            return null;
        }
        return roots.iterator().next();
    }

    public static <T> T saveEntity(final T entity) throws WebServiceException {
        final Object[] result = new Object[1];
        runInTransaction(new Runnable() { 
            @Override
            public void run() {
                final EntityManager em = EntityManagerUtils.getEntityManager();
                result[0] = em.merge(entity); 
            }
        });
        return ((T)result[0]);
    }

    public static <T> void removeEntity(final T entity) throws WebServiceException {
        runInTransaction(new Runnable() { 
            @Override
            public void run() {
                final EntityManager em = EntityManagerUtils.getEntityManager();
                em.remove(entity); 
            }
        });
    }
    
    public static void runInTransaction(final Runnable operation) {
        assert operation != null : "operation must not be null";
        final EntityManager em = EntityManagerUtils.getEntityManager();
        final EntityTransaction tx = em.getTransaction();
        if (tx.isActive()) {
            operation.run();
        } else {
            try {
                tx.begin();
                operation.run();
                tx.commit();
            }
            catch (final OptimisticLockException e) {
                throw new WebServiceException("Entity was updated by someone else. Reload and modify again!");
            }finally {
                if (tx.isActive()) {
                    tx.rollback();
                }
            }
        }
    }
}
